﻿using Enyim.Collections;
using Linh.Mvvm;
using Npgsql;
using NpgsqlTypes;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace DatabaseTunning
{
    internal class Data : IComparable<Data>
    {
        public Data()
        {
        }

        public Data(string value, double distance)
        {
            Value = value;
            Distance = distance;
        }

        public double Distance { get; set; }

        public string Value { get; set; }

        public int CompareTo(Data other)
        {
            if (other != null)
                return Distance.CompareTo(other.Distance);
            return 1;
        }
    }

    internal class MainWindowViewModel : BindableBase
    {
        private ObservableCollection<Data> _result = new ObservableCollection<Data>();
        private int algorithm;
        private int distanceMode;
        private int k = 10;
        private string loadText;
        private string place;
        private List<Data> result = new List<Data>();
        private double scale;
        private double time;
        private RTree<string> tree;
        private double x;
        private double y;

        private delegate double DistanceDelegate(NpgsqlPoint p, Envelope envelope);

        public int Algorithm
        {
            get { return algorithm; }
            set { SetProperty(ref algorithm, value); }
        }

        public int DistanceMode
        {
            get { return distanceMode; }
            set { SetProperty(ref distanceMode, value); }
        }

        public int K
        {
            get { return k; }
            set { SetProperty(ref k, value); }
        }

        public ICommand LoadCommand
        {
            get
            {
                return new DelegateCommand<object>((param) =>
                {
                    if (place != null)
                        ReadData(place);
                });
            }
        }

        public string LoadText
        {
            get { return loadText; }
            set { SetProperty(ref loadText, value); }
        }

        public string Place
        {
            get { return place; }
            set { SetProperty(ref place, value); }
        }

        public ObservableCollection<Data> Result
        {
            get { return _result; }
        }

        public ICommand RunCommand
        {
            get
            {
                return new DelegateCommand<object>(async (param) =>
                {
                    Result.Clear();
                    DistanceDelegate f = null;
                    switch (distanceMode)
                    {
                        case 0:
                            f = MinDist;
                            break;

                        case 1:
                            f = MinMaxDist;
                            break;
                    }
                    await Task.Delay(0);
                    result.Clear();
                    Stopwatch watch = new Stopwatch();
                    watch.Start();
                    switch (algorithm)
                    {
                        case 0:
                            SearchWithGreedy(new NpgsqlPoint(x, y), k, f);
                            break;

                        case 1:
                            SearchWithPriorityQueue(new NpgsqlPoint(x, y), k, f);
                            break;

                        case 2:
                            SearchWithGreedyOpen(new NpgsqlPoint(x, y), k, f, scale);
                            break;

                        case 3:
                            SearchWithGreedyOpen2(new NpgsqlPoint(x, y), k, f, scale);
                            break;
                    }
                    watch.Stop();
                    Time = watch.Elapsed.TotalMilliseconds;
                    foreach (var item in result)
                    {
                        Result.Add(item);
                    }
                    Debug.WriteLine("Average distance: {0}", result.Average(x => x.Distance));
                });
            }
        }

        public double Scale
        {
            get { return scale; }
            set { SetProperty(ref scale, value); }
        }

        public double Time
        {
            get { return time; }
            set { SetProperty(ref time, value); }
        }

        public double X
        {
            get { return x; }
            set { SetProperty(ref x, value); }
        }

        public double Y
        {
            get { return y; }
            set { SetProperty(ref y, value); }
        }

        private static double Distance(NpgsqlPoint p1, NpgsqlPoint p2)
        {
            return (p1.X - p2.X) * (p1.X - p2.X) + (p1.Y - p2.Y) * (p1.Y - p2.Y);
        }

        private static double MinDist(NpgsqlPoint p, Envelope envelope)
        {
            if (envelope.X1 <= p.X && envelope.X2 >= p.X && envelope.Y1 <= p.Y && envelope.Y2 >= p.Y)
                return 0;
            double r1, r2;
            if (p.X < envelope.X1)
                r1 = envelope.X1;
            else if (p.X > envelope.X2)
                r1 = envelope.X2;
            else r1 = p.X;
            if (p.Y < envelope.Y1)
                r2 = envelope.Y1;
            else if (p.Y > envelope.Y2)
                r2 = envelope.Y2;
            else r2 = p.Y;
            return Module(p.X - r1, p.Y - r2);
        }

        private static double MinMaxDist(NpgsqlPoint p, Envelope envelope)
        {
            if (envelope.X1 <= p.X && envelope.X2 >= p.X && envelope.Y1 <= p.Y && envelope.Y2 >= p.Y)
                return 0;
            var rm1 = p.X <= (envelope.X1 + envelope.X2) / 2 ? envelope.X1 : envelope.X2;
            var rM1 = p.X >= (envelope.X1 + envelope.X2) / 2 ? envelope.X1 : envelope.X2;
            var rm2 = p.Y <= (envelope.Y1 + envelope.Y2) / 2 ? envelope.Y1 : envelope.Y2;
            var rM2 = p.Y <= (envelope.Y1 + envelope.Y2) / 2 ? envelope.Y1 : envelope.Y2;
            return Math.Min(Module(p.X - rm1, p.Y - rM2), Module(p.Y - rm2, p.X - rM1));
        }

        private static double Module(double dx, double dy)
        {
            return Math.Sqrt(dx * dx + dy * dy);
        }

        private async void ReadData(string place)
        {
            LoadText = "Đang đọc..";
            await Task.Delay(1);
            tree = new RTree<string>(4);
            NpgsqlConnection conn = new NpgsqlConnection("Username=postgres; Password=123456; Database=postgres; Host=localhost;");
            conn.Open();
            NpgsqlCommand command = new NpgsqlCommand(string.Format("SELECT name,geom::POINT FROM placeasiapoints WHERE place='{0}'", place), conn);
            NpgsqlDataReader dr = command.ExecuteReader();
            int count = 0;
            List<RTreeNode<string>> list = new List<RTreeNode<string>>();
            while (dr.Read())
            {
                count++;
                string name = dr[0].ToString();
                NpgsqlPoint a = (NpgsqlPoint)dr[1];
                list.Add(new RTreeNode<string>(name, new Envelope(a.X, a.Y, a.X, a.Y)));
            }
            conn.Close();
            tree.Load(list);
            LoadText = string.Format("Đã đọc xong. Tổng cộng {0} bản ghi", count);
        }

        private void Search2(NpgsqlPoint p, int k, DistanceDelegate distance, RTreeNode<string> node)
        {
            List<KeyValuePair<RTreeNode<string>, double>> nodes = new List<KeyValuePair<RTreeNode<string>, double>>();
            foreach (var item in node.Children)
            {
                nodes.Add(new KeyValuePair<RTreeNode<string>, double>(item, distance(p, item.Envelope)));
            }
            nodes.Sort(Comparer<KeyValuePair<RTreeNode<string>, double>>.Create((x, y) => { return x.Value.CompareTo(y.Value); }));
            foreach (var item in nodes)
            {
                if (result.Count >= k)
                    return;
                if (item.Key.Data != null)
                {
                    result.Add(new Data(item.Key.Data, item.Value));
                }
                else Search2(p, k, distance, item.Key);
            }
        }

        private List<Data> Search3(NpgsqlPoint p, int k, DistanceDelegate distance, RTreeNode<string> node)
        {
            if (k == 0)
                return new List<Data>();
            List<KeyValuePair<RTreeNode<string>, double>> nodes = new List<KeyValuePair<RTreeNode<string>, double>>();
            foreach (var item in node.Children)
            {
                nodes.Add(new KeyValuePair<RTreeNode<string>, double>(item, distance(p, item.Envelope)));
            }
            nodes.Sort(Comparer<KeyValuePair<RTreeNode<string>, double>>.Create((x, y) => { return x.Value.CompareTo(y.Value); }));
            //nodes.Sum((x) => { return x.Value; });
            List<Data> temp = new List<Data>();
            foreach (var item in nodes)
            {
                if (temp.Count >= k)
                    return temp;
                if (item.Key.Data != null)
                {
                    temp.Add(new Data(item.Key.Data, item.Value));
                }
                else
                {
                    temp.AddRange(Search3(p, k / nodes.Count + 1, distance, item.Key));
                }
            }
            return temp;
        }

        private void SearchWithGreedy(NpgsqlPoint p, int k, DistanceDelegate distance)
        {
            Search2(p, k, distance, tree.Root);
        }

        private void SearchWithGreedyOpen(NpgsqlPoint p, int k, DistanceDelegate distance, double scale)
        {
            Search2(p, (int)(k * scale), distance, tree.Root);
            result.Sort();
            result = result.Take(k).ToList();
        }

        private void SearchWithGreedyOpen2(NpgsqlPoint p, int k, DistanceDelegate distance, double scale)
        {
            result = Search3(p, (int)(k * scale), distance, tree.Root);
            result.Sort();
            result = result.Take(k).ToList();
        }

        private void SearchWithPriorityQueue(NpgsqlPoint p, int k, DistanceDelegate distance)
        {
            int num = k;
            var node = tree.Root;
            PriorityQueue<KeyValuePair<RTreeNode<string>, double>> U = new PriorityQueue<KeyValuePair<RTreeNode<string>, double>>(Comparer<KeyValuePair<RTreeNode<string>, double>>.Create((a, b) => { return a.Value.CompareTo(b.Value); }));
            U.Push(new KeyValuePair<RTreeNode<string>, double>(node, distance(p, node.Envelope)));

            while (U.Count > 0)
            {
                var e = U.Pop();
                if (e.Key.Data == null)
                {
                    foreach (var item in e.Key.Children)
                    {
                        U.Push(new KeyValuePair<RTreeNode<string>, double>(item, distance(p, item.Envelope)));
                    }
                }
                else
                {
                    num--;
                    result.Add(new Data(e.Key.Data, e.Value));
                    if (num == 0)
                        return;
                }
            }
        }
    }
}